package ferp.android.activities;

import android.app.Activity;
import android.app.Dialog;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.view.Display;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import ferp.android.GUI;
import ferp.android.R;
import ferp.android.activities.preferences.Preferences;
import ferp.android.ads.MediationAdManager;
import ferp.android.ads.interstitial.InterstitialAdManager;
import ferp.android.dialogs.*;
import ferp.android.managers.DeckManager;
import ferp.android.managers.ProfileManager;
import ferp.android.social.Connector;
import ferp.android.tasks.center.TaskGameReportSend;
import ferp.android.tasks.local.TaskDeckLoader;
import ferp.android.views.table.TableView;
import ferp.core.ai.tree2.Score;
import ferp.core.card.Card;
import ferp.core.game.*;
import ferp.core.log.Log;
import ferp.core.player.Hand;
import ferp.core.player.Player;
import ferp.core.player.Profile;
import ferp.core.state.State;

import java.util.Arrays;

/**
 * User: olegs
 * Date: 30/10/2014 12:10
 */
public abstract class Main extends Activity implements Game.Listener,
                                                       TableView.Listener,
                                                       PoolDialog.Listener,
                                                       ErrorDialog.Listener,
                                                       RadioListViewDialog.Listener,
                                                       OfferDialog.Listener,
                                                       TaskGameReportSend.Listener,
                                                       ReportGameDialog.Listener,
                                                       MessageDialog.Listener,
                                                       ConfirmationDialog.Listener,
                                                       TaskDeckLoader.Listener,
                                                       IntroductionDialog.Listener
{
  @Override
  public void enableLastTrickMenu(boolean enabled)
  {
    if (miLastTrick != null)
      miLastTrick.setEnabled(enabled);
  }

  @Override
  public void showTrick(int trick) {table.showTrick(trick);}

  @Override
  public void setTrickCount(int player, int tricks) {table.setTrickCount(player, tricks);}

  @Override
  public void showTurn(int player) {table.showTurn(player);}

  @Override
  public void onBeforeShowInputOptions(Game g, Settings settings) {table.advanceController(g, settings);}

  @Override
  public void showBidPlay(Bid bid) {table.showBidPlay(bid);}
  @Override
  public void showBidMisere() {table.showBidMisere();}
  @Override
  public void showBidPass() {table.showBidPass();}
  @Override
  public void showBidPass(int factor) {table.showBidPass(factor);}
  @Override
  public void showDrop() {table.showDrop();}
  @Override
  public void showWithout3() {table.showWithout3();}
  @Override
  public void showContractPlay(Game g, Bid bid) {table.showContractPlay(g, bid);}
  @Override
  public void showWhistFull() {table.showWhistFull();}
  @Override
  public void showWhistHalf() {table.showWhistHalf();}
  @Override
  public void showWhistPass() {table.showWhistPass();}
  @Override
  public void showOpenStand() {table.showOpenStand();}
  @Override
  public void showTricking10Confirm() {table.showTricking10Confirm();}
  @Override
  public void showMisereConfirm() {table.showMisereConfirm();}
  @Override
  public void showOffer() {table.showOffer();}
  @Override
  public void enableCards(int player, int hand) {table.enablePlayerCards(player, hand);}

  @Override
  public void onItemSelected(Input.Offer offer)
  {
    Profile profile = profile();
    Game g = profile.game();

    g.input.score = g.options.score;
    g.input.offer = offer;

    table.onOfferDialogDismissed();

    // continue processing
    Game.log(g, this, "offer");
    process.now();
  }

  @Override
  public void onYesClick(int requestCode)
  {
    switch (requestCode)
    {
      case Dialogs.EXIT_CONFIRMATION_REGULAR:
        // finish the activity and go back to entrance screen
        exit();

        break;

      case Dialogs.CONFIRM_BID_PLAY:
      case Dialogs.CONFIRM_BID_MISERE:
      case Dialogs.CONFIRM_BID_PASS:
      case Dialogs.CONFIRM_CONTRACT_WITHOUT3:
      case Dialogs.CONFIRM_CONTRACT_PLAY:
        Game g = game();

        Game.log(g, this, "cc " + requestCode + ", bid=" + g.input.bid);

        confirmation = false;

        Game.log(g, this, "onYesClick");
        process.now();

        break;
    }
  }

  @Override
  public void onNoClick(int requestCode)
  {
    switch (requestCode)
    {
      case Dialogs.CONFIRM_BID_PLAY:
      case Dialogs.CONFIRM_BID_MISERE:
      case Dialogs.CONFIRM_BID_PASS:
      case Dialogs.CONFIRM_CONTRACT_WITHOUT3:
      case Dialogs.CONFIRM_CONTRACT_PLAY:
        Game g = game();

        Game.log(g, this, "ncc " + requestCode);

        confirmation = false;
        g.input.reset();

        break;
    }
  }

  @Override
  public void onCancel(int requestCode)
  {
    switch (requestCode)
    {
      case Dialogs.CONFIRM_BID_PLAY:
      case Dialogs.CONFIRM_BID_MISERE:
      case Dialogs.CONFIRM_BID_PASS:
      case Dialogs.CONFIRM_CONTRACT_WITHOUT3:
      case Dialogs.CONFIRM_CONTRACT_PLAY:
        Game g = game();

        Game.log(g, this, "ccc " + requestCode);

        confirmation = false;
        g.input.reset();

        break;

      case Dialogs.OFFER:
        table.onOfferDialogDismissed();
        break;
    }
  }

  @Override
  public void onBeforeDeal() {LoadingDialog.dismiss();}

  @Override
  public void onGameRestoreStart(Game g) {LoadingDialog.show(this, R.string.resuming_game);}

  @Override
  public void onGameRestoreEnd(Game g)
  {
    // call 'load' method to ensure interstitial ad loading as 'onAfterDeal' not called when game resumed
    InterstitialAdManager.load(this);

    LoadingDialog.dismiss();

    // always clear 'WAIT_FOR_RESUMING'
    g.options.clear(Input.Options.WAIT_FOR_RESUMING);

    if (!g.options.isHumanInputRequired())
    {
      Game.log(g, this, "onGameRestoreEnd");
      process.now();
    }
  }

  @Override
  public void onBeforeReplayStart(Game g)
  {
    LoadingDialog.dismiss();

    onAfterDeal(g);
  }

  @Override
  public void onSendReportRequested()
  {
    showDialog(Dialogs.REPORT_GAME);
  }

  @Override
  public void onAfterDeal(Game g)
  {
    Profile profile = profile();

    // set correct enable state to 'Last trick' context menu option
    enableLastTrickMenu(false);
    // reset table view
    table.reset(g, profile.settings, false);

    // load interstitial ad to be ready to show it when game is over
    InterstitialAdManager.load(this);
  }

  @Override
  public void onBiddingStart(Game g) {showFirstHand(g.getFirstHandPlayer());}

  @Override
  public void showBid(Player p, Game.BidData bid, boolean animation) {table.showBid(p, bid, animation);}

  @Override
  public void hideBids(Game g) {table.hideBids(g);}

  @Override
  public void onPlayerBid(Player p, Game.BidData bid)
  {
    table.showBid(p, bid, true);

    //if (p != pm.profile().game.human())
    process.delay = Settings.Delays.AFTER_BID;

    Log.debug(Log.TAG, p.name() + ": " + bid.toString());
  }

  @Override
  public void onTalonShow(Game g, Settings settings)
  {
    // show talon cards
    table.openTalon();

    // wait for touch
    g.options.set(Input.Options.WAIT_FOR_TOUCH);

    // set longer delay for next process()
    //process.delay = pm.profile().game.declarer == pm.profile().game.human() ? Process.Delay.AFTER_HUMAN_TALON_OPEN : Process.Delay.AFTER_AI_TALON_OPEN;
  }

  @Override
  public void onTalonTaken(Game g, Settings settings)
  {
    table.hideTalon();
    table.showPlayerCards(g, settings, g.player.declarer);
  }

  @Override
  public void onPlayerContract(Player p, Game.BidData bid)
  {
    table.showBid(p, bid, true);

    //if (p != pm.profile().game.human())
    process.delay = Settings.Delays.AFTER_CONTRACT;

    Log.debug(Log.TAG, p.name() + ": " + bid.toString());
  }

  @Override
  public void onPlayerDrop(Game g, Settings settings, Player p)
  {
    table.showPlayerCards(g, settings, p.id());
    table.enablePlayerCards(p.id(), false);

    if (p != g.human())
      process.delay = Settings.Delays.AFTER_DROP;
  }

  @Override
  public void onOpenGameType(Game g, Settings settings, Player opponent1, Player opponent2)
  {
    Log.debug(Log.TAG, "laying down");

    table.showPlayerCards(g, settings, opponent1);
    table.showPlayerCards(g, settings, opponent2);
  }

  @Override
  public void onStandingGameType() {GUI.toast(this, R.string.playing_blindly, Toast.LENGTH_LONG);}

  @Override
  public void onPlayCard(Game g, Player player, Card card)
  {
    table.hideClock();

    // start card animation, next process() will be called on animation end
    table.moveCardToTrick(player.id(), card, true);

    process.delay = Settings.Delays.AFTER_CARD_PLAYED;
  }

  @Override
  public void showPlayerCards(Game g, Settings settings, int player) {table.showPlayerCards(g, settings, player);}

  @Override
  public void disablePlayerCards(int player) {table.enablePlayerCards(player, false);}

  /*
  public void onCardPlayed(Game g, Player mover, Player current, Card card, boolean automatic)
  {
    table.hideClock();

    // put the card on the corresponding trick card; at this point it's already done for human mover
    // unless the move is obvious and the player has the corresponding setting on
    // otherwise we do it only for android
    if (automatic)
      table.moveCardToTrick(current.id(), card, true);

    // AI plays misere - mark played card
    if (g.bid != null && g.bid.isMisere() && g.declarer != g.human() && g.declarer == current)
      table.markMisereDesk(card);

    process.delay = Process.Delay.AFTER_CARD_PLAYED;
  }
*/

  @Override
  public void onAfterMovingRoundEnd(Game g, Settings settings, Player winner)
  {
    if (table != null)
    {
      table.updateTrickCount(g, winner);
      table.hideTrick(g, settings);
    }

    process.delay = Settings.Delays.AFTER_ROUND_END;
  }

  @Override
  public void onTalonCardPlayed(Settings settings, int round)
  {
    table.openTalonCard(round);
  }

  @Override
  public void onScore(Game g)
  {
    // wait for pool activity to close, then the processing should be resumed (see onActivityResult())
    g.options.set(Input.Options.WAIT_FOR_POOL);

    // show pool dialog
    PoolDialog.show(this, Dialogs.POOL, false);
  }

  @Override
  public void onShowInitialPosition(Game g, Settings settings)
  {
    table.showInitialPosition(g, settings);

    GUI.toast(this, R.string.initial_position);
  }

  @Override
  public void onPoolDialogDismiss(Input.PoolUserChoice puc)
  {
    Game g = game();

    // set input
    g.input.puc = puc;

    // remove the pool dialog to force its creation next time it opened - it must be recreated because of animation
    removeDialog(Dialogs.POOL);

    // resume processing in case we are in wait for pool mode, otherwise end the game
    if (g.options.isSet(Input.Options.WAIT_FOR_POOL))
    {
      Game.log(g, this, "onPoolDialogDismiss");
      process.now();
    }
  }

  @Override
  public void onCloseClick() {entrance();
  }

  @Override
  public void onOfferButtonClicked() {showDialog(Dialogs.OFFER);
  }

  @Override
  public void onOfferDeclined() {GUI.toast(this, R.string.offer_denied);}

  @Override
  public void onCardReleased(Card c)
  {
    Game g = game();

    // hide glove
    table.hideTurn();
    //table.showClock(g.current.player);

    table.enablePlayerCards(g.player.current, false);
    table.moveCardToTrick(g.player.current, c, false);
  }

  @Override
  public void onCardCommit(Card c)
  {
    Game g = game();

    //Log.debug(Log.TAG, "card animation ended");
    g.input.card = c.hash;

    // if still resolving do not process
    if (isResolving())
    {
      Log.debug(Log.TAG, "resolving is still in process!");

      table.showClock(g, g.player.current);
    }
    else
    {
      Game.log(g, this, "onCardCommit");
      process.now();
    }
  }

  @Override
  public void onTouch()
  {
    Game g = game();

    // continue processing
    if (g != null && g.options.isSet(Input.Options.WAIT_FOR_TOUCH))
    {
      // clear options
      g.options.clear();

      Game.log(g, this, "onTouch");
      process.now();
    }
  }

  @Override
  public void onInputButtonClicked(View view, int id)
  {
    // check confirmation flag to avoid double processing,
    // i.e. when confirmation dialog shown process is not called,
    // which means input buttons will be shown and two onClick events might arrive
    if (view.isShown() && !confirmation)
    {
      Profile profile = profile();
      Game g = game();

      switch (id)
      {
        case R.id.pass:
          g.input.bid.type = Bid.Type.PASS;

          if (confirm(Dialogs.CONFIRM_BID_PASS, profile.settings.confirmations.bid, Confirmation.pass))
            return;

          Game.log(g, this, "pass button " + g.input.bid);
          break;

        case R.id.whist_pass:
          g.input.bid.type = Bid.Type.PASS;
          Game.log(g, this, "whist pass button " + g.input.bid);
          break;

        case R.id.whist_half:
          g.input.bid.type = Bid.Type.HALF_WHIST;
          Game.log(g, this, "whist half button " + g.input.bid);
          break;

        case R.id.whist_full:
          g.input.bid.type = Bid.Type.WHIST;
          Game.log(g, this, "whist full button " + g.input.bid);
          break;

        case R.id.misere:
          g.input.bid.type = Bid.Type.MISERE;

          if (confirm(Dialogs.CONFIRM_BID_MISERE, profile.settings.confirmations.bid, Confirmation.misere))
            return;

          Game.log(g, this, "misere button " + g.input.bid);
          break;

        case R.id.bid:
          g.input.bid.assign(g.options.bid);

          if (confirm(Dialogs.CONFIRM_BID_PLAY, profile.settings.confirmations.bid, Confirmation.play))
            return;

          Game.log(g, this, "bid button " + g.input.bid);
          break;

        case R.id.drop:
          int drop = table.getDrop();

          // assign dropped cards
          g.input.drop[0] = Hand.first(drop);
          g.input.drop[1] = Hand.last(drop);

          Game.log(g, this, "drop button");
          break;

        case R.id.declare:
          g.input.bid.assign(g.options.bid);

          if (confirm(Dialogs.CONFIRM_CONTRACT_PLAY, profile.settings.confirmations.contract, Confirmation.contract))
            return;

          Game.log(g, this, "declare button, declared " + g.input.bid);
          break;

        case R.id.stand:
        case R.id.stand2:
          g.input.stand = true;
          Game.log(g, this, "stand button " + g.input.stand);
          break;

        case R.id.lay_down:
        case R.id.lay_down2:
          g.input.stand = false;
          Game.log(g, this, "lay down button " + g.input.stand);
          break;

        case R.id.without3:
          g.input.bid.type = Bid.Type.WITHOUT_3;

          if (confirm(Dialogs.CONFIRM_CONTRACT_WITHOUT3, profile.settings.confirmations.contract, Confirmation.without3))
            return;

          Game.log(g, this, "without 3 button " + g.input.bid);
          break;

        case R.id.tricking10_agree:
          g.input.check10 = false;
          Game.log(g, this, "tricking10 agree button " + g.input.check10);
          break;

        case R.id.tricking10_play:
          g.input.check10 = true;
          Game.log(g, this, "tricking10 play button " + g.input.check10);
          break;

        case R.id.misere_play:
          g.input.undoDrop = false;
          Game.log(g, this, "misere play button, undo drop=" + g.input.undoDrop);
          break;
      }

      // process the current state if confirmation not required
      Game.log(g, this, "onInputButtonClicked");
      process.now();
    }
  }

  @Override
  public void onUndoDropButtonClicked(Game g)
  {
    Game.log(g, this, "onUndoDropButtonClicked");

    g.input.undoDrop = true;
    process.now();
  }

  @Override
  public void onShareClick()
  {
    showDialog(Dialogs.SHARE);
  }

  @Override
  public void onReplayStart()
  {
    GUI.toast(this, R.string.toast_replay_start);
  }

  @Override
  public void onReplayEnd()
  {
    GUI.toast(this, R.string.toast_replay_end);
  }

  @Override
  public void markBidPlay() {table.markBidPlay();}
  @Override
  public void markBidMisere() {table.markBidMisere();}
  @Override
  public void markBidPass() {table.markBidPass();}
  @Override
  public void markDrop() {table.markDrop();}
  @Override
  public void markWhistFull() {table.markWhistFull();}
  @Override
  public void markWhistHalf() {table.markWhistHalf();}
  @Override
  public void markWhistPass() {table.markWhistPass();}
  @Override
  public void markMiserePlay() {table.markMiserePlay();}

  @Override
  public void markStand(boolean stand)
  {
    if (stand)
      table.markStand();
    else
      table.markLaydown();
  }

  @Override
  public void markCard(int player, Card c, int i) {table.markCard(player, c, i);}
  @Override
  public void markCard(int player, Card c) {markCard(player, c, 0);}

  @Override
  public void onInvalidBid()
  {
    GUI.toast(this, R.string.tutorial_invalid_bid, Color.RED, Toast.LENGTH_LONG);
  }

  @Override
  public void onInvalidDrop()
  {
    table.resetDrop();

    GUI.toast(this, R.string.tutorial_invalid_drop, Color.RED, Toast.LENGTH_LONG);
  }

  @Override
  public void onInvalidContract()
  {
    GUI.toast(this, R.string.tutorial_invalid_contract, Color.RED, Toast.LENGTH_LONG);
  }

  @Override
  public void onInvalidGameType()
  {
    GUI.toast(this, R.string.tutorial_invalid_game_type, Color.RED, Toast.LENGTH_LONG);
  }

  @Override
  public void onInvalidMove(Game g, Settings settings, int player)
  {
    table.undoMove(player);

    GUI.toast(this, R.string.tutorial_invalid_move, Color.RED, Toast.LENGTH_LONG);
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item)
  {
    switch (item.getItemId())
    {
      case R.id.main_last_trick:
        showDialog(Dialogs.LAST_TRICK);
        return true;

      case R.id.main_report_game:
        showDialog(Dialogs.REPORT_GAME);
        return true;

      case R.id.main_help:
        startActivity(new Intent(this, Help.class));
        return true;

      default:
        return super.onOptionsItemSelected(item);
    }
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu)
  {
    getMenuInflater().inflate(R.menu.main, menu);

    miLastTrick = menu.findItem(R.id.main_last_trick);

    return true;
  }

  @Override
  public boolean onPrepareOptionsMenu(Menu menu)
  {
    // set correct enable state to 'Last trick' option each time the context menu is opened
    miLastTrick.setEnabled(game().trick.last != 0);

    return true;
  }

  @Override
  public void onConfigurationChanged(Configuration newConfig)
  {
    Preferences.setLocale(this, profile());

    super.onConfigurationChanged(newConfig);
  }

  @Override
  public void onSendingGameReportStart()
  {
    showDialog(Dialogs.REPORT_SUBMIT);
  }

  @Override
  public void onSendingGameReportEnd(boolean success)
  {
    GUI.tryToDismissDialog(this, Dialogs.REPORT_SUBMIT);
    GUI.toast(this, success ? R.string.report_game_success : R.string.report_game_failure);
  }

  @Override
  public void onSendClick(String email, String description)
  {
    new TaskGameReportSend(profile(), getProfileManager().version(), email, description, null, this).execute();
  }

  @Override
  public void finish()
  {
    destroyTable();

    super.finish();
  }

  @Override
  public void onDeckLoadStart(String deck)
  {
    LoadingDialog.showDeck(this, deck);
  }

  @Override
  public void onDeckLoadFinished(DeckManager dm, String deck) {tryToResumeGame();}

  @Override
  public void onBestScoreRequested(Game g) throws Game.Error
  {
    if (isResolverAvailable())
      g.options.score = (short)getBestScore();
  }

  @Override
  public void onSolveRequested(Game g, Settings settings, Player mover) throws Game.Error
  {
    if (isResolverAvailable())
    {
      int score = getBestScore();

      // show clock only when resolver still really needs time to complete the task
      if (!Score.isComplete((short)score))
        table.showClock(g, mover.id());

      // start resolver
      resolve(mover);
    }
  }

  @Override
  public void onTreeReady(Player mover) throws Game.Error
  {
    Profile profile = profile();
    Game g = profile.game();
    int depth = Levels.getBestMoveDepth(g, profile.settings, mover);

    if (table != null && isResolverAvailable() && !stopped)
    {
      table.hideClock();

      byte move = getBestMove(g, depth);
      Card card = Card.card(move);

      if (mover == g.human())
      {
        Log.debug(Log.TAG, "suggestion for " + g.player(g.player.current) + " [" + Hand.dump(g.getCurrentPlayerHand().current) + "]: " + card + ", mover: " + mover.id());

        if (g.input.card == 0)
          Log.debug(Log.TAG, "waiting for the human...");
        else
        {
          Game.log(g, this, "onTreeReady");
          process.now();
        }
      }
      else
        onPlayCard(g, g.current(), card);
    }
  }

  @Override
  public void onResolverAdvance(Game g, Settings settings, int move) throws Game.Error
  {
    // redraw player's hand to maintain suit order
    table.showPlayerCards(g, settings, g.player.current);

    // we get there after card played, schedule next process with a delay
    process.delay = Settings.Delays.AFTER_CARD_PLAYED;
  }

  @Override
  public void setTalon(int[] talon) {table.setTalon(talon);
  }
  @Override
  public void closeTalonCard(int i) {table.closeTalonCard(i);
  }
  @Override
  public void openTalonCard(int i) {table.openTalonCard(i);
  }
  @Override
  public void hideTalonCard(int i) {table.hideTalonCard(i);
  }

  @Override
  public void showMisereDesk(Game g, Settings settings, int hand) {table.showMisereDesk(g, settings, hand);
  }
  @Override
  public void markMisereMove(int move) {table.markMisereDesk(Card.card(move));
  }

  @Override
  public void showFirstHand(int player) {table.showFirstHand(player);
  }
  @Override
  public void hideFirstHand() {table.hideFirstHand();}

  protected abstract ProfileManager getProfileManager();

  protected abstract boolean isResolverAvailable();
  protected abstract boolean isResolving();
  protected abstract int getBestScore() throws Game.Error;
  protected abstract void resolve(Player mover) throws Game.Error;
  protected abstract byte getBestMove(Game g, int depth) throws Game.Error;

  protected void notifyResolver() {/** nothing to do **/}

  @Override
  protected void onCreate(Bundle savedInstanceState)
  {
    Profile profile = profile();
    String name = ((Object)this).getClass().getSimpleName();

    // set correct orientation depends on if running on tablet or not
    GUI.setOrientation(this, profile);

    super.onCreate(savedInstanceState);

    // this is to prevent incorrect activity restoring, when profile or game objects might be null
    if (profile == null || profile.game() == null)
    {
      Log.debug(Log.TAG, name + " activity finished. Profile = " + profile + ", game = " + (profile == null ? "n/a" : profile.game()));
      exit();

      return;
    }

    // restore state
    String message = name + " activity " + (savedInstanceState == null ? "created" : "restored") + ", pid=" + android.os.Process.myPid() + ", tid=" + Thread.currentThread().getId();
    Game g = profile.game();

    Log.debug(Log.TAG, message);
    Game.log(g, this, message);

    // show the inflated main layout
    Preferences.setLocale(this, profile);

    setContentView(R.layout.main);

    // resolve table view
    table = (TableView)findViewById(R.id.table);

    // determine whether we are dealing with square screen
    Display d = getWindowManager().getDefaultDisplay();
    int w = d.getWidth(), h = d.getHeight();

    // handle square screens
    table.initialize(this, getProfileManager(), w == h); //|| Math.max(w, h) * 0.9f <= Math.min(w, h));

    // initialize rest resources only in case 'onCreate' phase has been completed successfully
    adManager = new MediationAdManager(this, profile());
  }

  @Override
  protected void onResume()
  {
    Log.debug(Log.TAG, "resuming " + name() + " activity");

    super.onResume();

    adManager.resume();
  }

  @Override
  protected void onRestart()
  {
    restarting = true;

    super.onRestart();
  }

  @Override
  protected void onStart()
  {
    Profile profile = profile();
    String message = (restarting ? "re" : "") + "starting activity";

    Log.debug(Log.TAG, message);
    Game.log(game(), this, message);

    // clear restarting flag
    restarting = false;
    // activity is not stopped anymore
    stopped = false;
    // game is not resumed yet
    resumed = false;

    // clear messages and animations from previous cycle
    removeEvents();

    // show "loading" dialog
    LoadingDialog.show(this, R.string.preparing_data);

    // load card decks asynchronously
    TaskDeckLoader.execute(this, this, profile.settings.deck);

    super.onStart();
  }

  @Override
  protected void onPause()
  {
    Log.debug(Log.TAG, "pausing " + name() + " activity");

    super.onPause();

    adManager.pause();
  }

  @Override
  protected void onStop()
  {
    String message = "stopping " + name() + " activity";

    super.onStop();

    adManager.stop();

    try
    {
      // we have to reload the current profile to rollback the game state to the last consistent save point
      getProfileManager().reloadCurrentProfile();
    }
    catch (Exception e)
    {
      Log.error(Log.TAG, e);
      showDialog(Dialogs.ALERT_ERROR);
    }

    Log.debug(Log.TAG, message);
    Game.log(game(), this, message);

    // clean things up: remove events, dialogs and cancel the resolver task
    clean();

    stopped = true;
  }

  @Override
  protected void onDestroy()
  {
    Log.debug(Log.TAG, name() + " activity destroyed");

    super.onDestroy();

    // destroy the ADs
    if (adManager != null)
      adManager.destroy();

    destroyTable();
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data)
  {
    super.onActivityResult(requestCode, resultCode, data);

    switch (requestCode)
    {
      case Preferences.REQUEST_CODE:
        applySettings();
        break;

      case Connector.RequestCode.VK:
        Connector.vk.onActivityResult(this, resultCode, data);
        break;

      case Connector.RequestCode.TW:
        Connector.tw.onActivityResult(this, resultCode, data);
        break;

      case Connector.RequestCode.FB:
        Connector.fb.onActivityResult(this, resultCode, data);
        break;

    }
  }

  @Override
  protected Dialog onCreateDialog(int id)
  {
    switch (id)
    {
      case Dialogs.ALERT_ERROR:
        return new ErrorDialog(this, this);

      case Dialogs.NOT_ENOUGH_RESOURCES:
        return new ErrorDialog(this, this, R.string.dialog_not_enough_resources_message);

      case Dialogs.OFFER:
        return OfferDialog.create(this, this, profile(), id);

      case Dialogs.LAST_TRICK:
        return LastTrickDialog.create(this, profile());

      case Dialogs.POOL:
        return PoolDialog.create(this, this, profile());

      case Dialogs.REPORT_SUBMIT:
        ProgressDialog progress = new ProgressDialog(this);
        // allow dialog cancellation (the task will continue to execute)
        progress.setCancelable(true);
        progress.set(R.string.please_wait, R.string.report_game_progress);

        return progress;

      case Dialogs.REPORT_GAME:
        return new ReportGameDialog(this, this);

      case Dialogs.EXIT_CONFIRMATION_REGULAR:
        return new MessageDialog(this, this, id);

      case Dialogs.EXIT_CONFIRMATION_REPLAY:
      case Dialogs.EXIT_CONFIRMATION_TUTORIAL:
        return new RadioListViewDialog(this, this, id);

      case Dialogs.CONFIRM_BID_PLAY:
      case Dialogs.CONFIRM_BID_MISERE:
      case Dialogs.CONFIRM_BID_PASS:
      case Dialogs.CONFIRM_CONTRACT_WITHOUT3:
      case Dialogs.CONFIRM_CONTRACT_PLAY:
        return ConfirmationDialog.create(this, this, id);

      case Dialogs.SHARE:
        return ShareDialog.create(this, profile());
    }

    return super.onCreateDialog(id);
  }

  @Override
  protected void onPrepareDialog(int id, Dialog dialog)
  {
    Profile profile = profile();
    Game g = profile.game();
    Resources resources = getResources();

    Preferences.setLocale(this, profile);

    super.onPrepareDialog(id, dialog);

    switch (id)
    {
      case Dialogs.OFFER:
        ((OfferDialog)dialog).prepare();
        break;

      case Dialogs.LAST_TRICK:
        ((LastTrickDialog)dialog).prepare();
        break;

      case Dialogs.REPORT_GAME:
        ((ReportGameDialog)dialog).reset();
        break;

      case Dialogs.EXIT_CONFIRMATION_REGULAR:
        ((MessageDialog)dialog).set(R.string.warning, resources.getString(R.string.warning_exit_game));
        break;

      case Dialogs.EXIT_CONFIRMATION_REPLAY:
        ((RadioListViewDialog)dialog).set(resources.getString(R.string.dialog_exit_replay_title), Arrays.asList(resources.getStringArray(R.array.dialog_exit_options_replay)));
        break;

      case Dialogs.EXIT_CONFIRMATION_TUTORIAL:
        ((RadioListViewDialog)dialog).set(resources.getString(R.string.dialog_exit_replay_title), Arrays.asList(resources.getStringArray(R.array.dialog_exit_options_tutorial)));
        break;

      case Dialogs.CONFIRM_BID_PLAY:
        ((ConfirmationDialog)dialog).set(R.string.dialog_confirmation_title_bid_play,
            resources.getString(R.string.dialog_confirmation_text_bid_play1),
            g.input.bid,
            resources.getString(R.string.dialog_confirmation_text_bid_play2),
            profile.settings.confirmations, ConfirmationDialog.bcc, profile.settings.confirmations.bid);
        break;

      case Dialogs.CONFIRM_BID_MISERE:
        ((ConfirmationDialog)dialog).set(R.string.dialog_confirmation_title_bid_misere, resources.getString(R.string.dialog_confirmation_text_bid_misere), profile.settings.confirmations, ConfirmationDialog.bcc, profile.settings.confirmations.bid);
        break;

      case Dialogs.CONFIRM_BID_PASS:
        ((ConfirmationDialog)dialog).set(R.string.dialog_confirmation_title_bid_pass, resources.getString(R.string.dialog_confirmation_text_bid_pass), profile.settings.confirmations, ConfirmationDialog.bcc, profile.settings.confirmations.bid);
        break;

      case Dialogs.CONFIRM_CONTRACT_WITHOUT3:
        ((ConfirmationDialog)dialog).set(R.string.dialog_confirmation_title_contract_without3, resources.getString(R.string.dialog_confirmation_text_contract_without3), profile.settings.confirmations, ConfirmationDialog.ccc, profile.settings.confirmations.contract);
        break;

      case Dialogs.CONFIRM_CONTRACT_PLAY:
        ((ConfirmationDialog)dialog).set(R.string.dialog_confirmation_title_contract_play,
            resources.getString(R.string.dialog_confirmation_text_contract_play1),
            g.input.bid,
            resources.getString(R.string.dialog_confirmation_text_contract_play2),
            profile.settings.confirmations, ConfirmationDialog.ccc, profile.settings.confirmations.contract);
        break;

      case Dialogs.SHARE:
        ((ShareDialog)dialog).prepare();
        break;
    }
  }

  @Override
  protected void onPrepareDialog(int id, Dialog dialog, Bundle args)
  {
    Profile profile = profile();

    Preferences.setLocale(this, profile);

    super.onPrepareDialog(id, dialog, args);

    switch (id)
    {
      case Dialogs.POOL:
        ((PoolDialog)dialog).set(profile, args);
        break;
    }
  }

  // the method must be called when no error occurred to prevent activity closing when error dialog shown
  protected void exit()
  {
    if (!exiting)
    {
      terminate();
      entrance();
    }
  }

  protected void exit(Exception e, String where, int dialog, boolean report)
  {
    if (!exiting)
    {
      terminate();

      showDialog(dialog);

      if (report)
        new TaskGameReportSend(profile(), getProfileManager().version(), ReportGameDialog.getEmail(getApplication(), "runtime@exception.com"), where, e, null).execute();

      if (e != null)
        Log.error(Log.TAG + "/Exit", e);
    }
  }

  protected void terminate()
  {
    // perform exit logic only once
    exiting = true;
    // set paused flag to false to prevent processing
    stopped = true;

    clean();
  }

  protected void clean()
  {
    // remove all callbacks and animations
    removeEvents();

    // remove all dialogs
    for (int id : Dialogs.set)
      removeDialog(id);

    if (table != null)
      table.onOfferDialogDismissed();

    // clear confirmation flag
    confirmation = false;
  }

  protected Profile profile() {return getProfileManager().getCurrentProfile();}
  protected Game game() {return profile().game();}

  protected boolean isGameReadyForResume() {return !stopped && !resumed && DeckManager.instance(this).isReady();}

  protected void resumeGame(Profile profile)
  {
    // resume game
    profile.resumeGame();

    Game.log(profile.game(), this, "resumeGame");
    process.schedule(0);
  }

  protected void tryToResumeGame()
  {
    if (isGameReadyForResume())
    {
      Profile profile = profile();

      // start processing
      if (profile != null && profile.game() != null)
        resumeGame(profile);
      else
        exit();

      resumed = true;
    }
  }

  protected void removeEvents()
  {
    process.clear();

    if (table != null)
      table.removeAnimations();
  }

  protected void handleException(Exception e)
  {
    exit(e, "processing stopped due to unexpected exception!", Dialogs.ALERT_ERROR, true);
  }

  // perform delayed processing in the main UI thread
  protected class Process implements Runnable
  {
    protected Handler handler = new Handler();
    protected long delay;

    // process now
    protected void now()
    {
      // clear options
      //game().options.clear();

      // hide options first and process almost immediately
      if (table != null)
      {
        table.hideOptions();
        handler.postDelayed(this, 1);
      }
    }

    // schedule delayed processing
    protected void schedule(long delay)
    {
      Log.debug(Log.TAG, "scheduling next process in " + delay  + " ms");

      // schedule the processing event
      if (table != null)
        handler.postDelayed(this, delay);
    }

    protected void clear()
    {
      handler.removeCallbacks(this);
    }

    public void run()
    {
      // set delay to the default
      delay = Settings.Delays.DEFAULT;

      Profile profile = profile();
      Game g = profile.game();

      if (g != null && g.state() != null && g.mode() != null && !stopped)
      {
        Settings settings = profile.settings;
        State state = g.state();

        try
        {
          String m = g.mode().getClass().getSimpleName();
          String s = state.getClass().getSimpleName();
          String n = state.next() == null ? null : state.next().getClass().getSimpleName();
          String pid = g.current() == null ? null : String.valueOf(g.current().id());

          if (state == State.resume)
            s += "(" + n + ")";

          Game.log(g, Main.this, m + "|" + s + ", p/" + pid + ", " +
              "o=[f/" + g.options.flag() + ",h/" + g.options.hand + ",b/" + g.options.bid + ",s/" + g.options.score + "], " +
              "i=[b/" + g.input.bid + ",p/" + g.input.puc + ",o/" + g.input.offer + ",d/" + g.input.drop[0] + "&" + g.input.drop[1] + ",c/" + g.input.card + "]");

          int r = state.execute(Main.this, profile, g, settings, g.input);

          if (r != State.Result.SHOW_OPTIONS)
          {
            Game.log(profile.game(), Main.this, "run");
            schedule(Math.max(r, delay));
          }

          notifyResolver();
        }
        catch (Exception e)
        {
          handleException(e);
        }
      }
      else
      {
        String message = "process run skipped - p/" + stopped + ", g/" + g + ", s/" + (g == null ? null : g.state()) + ", m/" + (g == null ? null : g.mode());

        Log.debug(Log.TAG, message);
        Game.log(game(), Main.this, message);
      }
    }
  }

  protected static final class Dialogs
  {
    // error dialog id
    protected static final int ALERT_ERROR = 1;
    // not enough memory dialog id
    protected static final int NOT_ENOUGH_RESOURCES = 2;

    // offer dialog id
    protected static final int OFFER = 3;
    // previous trick dialog id
    protected static final int LAST_TRICK = 4;
    // pool dialog id
    protected static final int POOL = 5;
    // progress dialog
    protected static final int REPORT_SUBMIT = 6;
    // report game
    protected static final int REPORT_GAME = 7;
    // rate application
    protected static final int RATE_APPLICATION = 8;

    // exit confirmation dialogs
    protected static final int EXIT_CONFIRMATION_REGULAR  = 9;
    protected static final int EXIT_CONFIRMATION_REPLAY   = 10;
    protected static final int EXIT_CONFIRMATION_TUTORIAL = 11;

    // fast moving
    protected static final int FAST_MOVING = 12;

    // confirmation dialogs
    protected static final int CONFIRM_BID_PLAY = 13;
    protected static final int CONFIRM_BID_MISERE = 14;
    protected static final int CONFIRM_BID_PASS = 15;
    protected static final int CONFIRM_CONTRACT_WITHOUT3 = 16;
    protected static final int CONFIRM_CONTRACT_PLAY = 17;

    // share
    protected static final int SHARE = 18;

    // tricking '10' check dialog
    protected static final int TRICKING_10_CHECK = 19;

    // poll dialog
    protected static final int POLL = 20;

    // poll submit dialog
    protected static final int POLL_SUBMIT = 21;

    // tutorial introduction
    protected static final int INTRODUCTION = 22;

    // all dialogs id's set
    protected static final int set[] = new int[] {
        ALERT_ERROR, NOT_ENOUGH_RESOURCES, OFFER, LAST_TRICK, POOL, REPORT_SUBMIT, REPORT_GAME, RATE_APPLICATION,
        EXIT_CONFIRMATION_REGULAR, EXIT_CONFIRMATION_REPLAY, FAST_MOVING, CONFIRM_BID_PLAY, CONFIRM_BID_MISERE,
        CONFIRM_BID_PASS, CONFIRM_CONTRACT_WITHOUT3, CONFIRM_CONTRACT_PLAY, SHARE, TRICKING_10_CHECK, POLL, POLL_SUBMIT,
        INTRODUCTION
    };
  }

  // main process object
  protected final Process process = new Process();

  // views and menu items
  protected TableView table;
  protected MenuItem miLastTrick;

  // helper flags
  protected boolean stopped;
  protected boolean resumed;
  protected boolean restarting;
  protected boolean exiting;
  protected boolean confirmation;

  // mediation manager
  protected MediationAdManager adManager;

  private boolean confirm(int dialogID, Settings.Confirmation confirmation, Confirmation c)
  {
    Profile profile = profile();
    Game g = profile.game();

    if (c.isRequired(confirmation, g, profile.settings, g.input.bid))
    {
      this.confirmation = true;

      showDialog(dialogID);
      return true;
    }

    return false;
  }

  private void applySettings() {table.applySettings();}

  protected void entrance()
  {
    finish();
    startActivity(new Intent(getApplicationContext(), Entrance.class));
  }

  private void destroyTable()
  {
    // remove postDelayed runnable, otherwise the scheduled event might arrive
    // when reopen the activity too fast and raise condition could happen
    removeEvents();

    DeckManager.instance(this).clear();

    // trigger memory release
    table = null;

    Log.debug(Log.TAG, "Table destroyed");
  }

  private String name() {return ((Activity)this).getClass().getSimpleName();}
}
